package com.gambi.example.tapestry.services;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.tapestry5.internal.services.AssetResourceLocator;
import org.apache.tapestry5.ioc.Resource;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.ContextPathEncoder;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestHandler;
import org.slf4j.Logger;

import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;

public class GWTServletHttpRequestFilter implements HttpServletRequestFilter,
// RequestFilter,
		SerializationPolicyProvider {

	private final Logger logger;
	private final AssetResourceLocator assetResourceLocator;
	private final ContextPathEncoder contextPathEncoder;

	private HttpServletRequest req;

	public GWTServletHttpRequestFilter(
			AssetResourceLocator assetResourceLocator,
			ContextPathEncoder contextPathEncoder, Logger logger) {
		// this.requestGlobals = requestGlobals;
		this.assetResourceLocator = assetResourceLocator;
		this.contextPathEncoder = contextPathEncoder;
		this.logger = logger;
	}

	public boolean service(HttpServletRequest request,
			HttpServletResponse response, HttpServletRequestHandler handler)
			throws IOException {

		this.req = request;
		String contentType = request.getHeader("Content-Type");
		if (contentType == null || !contentType.contains("text/x-gwt-rpc")) {
			return handler.service(request, response);
		}
		try {

			HttpServletRequest newRequest = createWrappedRequest(request);
			System.out.println("GWTServletHttpRequestFilter.service() "
					+ newRequest.getServletPath());

			final GwtOutputStream gwtOutputStream = new GwtOutputStream(
					newRequest, response);

			HttpServletResponse newResponse = new HttpServletResponseWrapper(
					response) {

				@Override
				public ServletOutputStream getOutputStream() throws IOException {
					return gwtOutputStream;
				}
			};

			boolean handled = handler.service(newRequest, newResponse);

			if (handled) {
				// gwtOutputStream.close();
			}

			return handled;

		} catch (ServletException e) {
			logger.error("servlet problem {}", e);
			// throw new IOException("Servlet Problem " + e.getMessage());
			throw new RuntimeException(e);

		} catch (IOException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		} catch (SerializationException e) {
			logger.error("servlet problem {}", e);
			throw new RuntimeException(e);
		}
	}

	protected HttpServletRequest createWrappedRequest(HttpServletRequest request)
			throws ServletException, IOException, SerializationException {

		// Parse and decode the request
		String requestPayload = readContent(request);
		final RPCRequest rpcRequest = decodePayload(request, requestPayload);
		System.out
				.println("GWTServletHttpRequestFilter.createWrappedRequest() "
						+ Arrays.toString(rpcRequest.getParameters()));
		HttpServletRequest newRequest = new HttpServletRequestWrapper(request) {

			// Encode the parameters as context URL, this may require new
			// coercions to be defined
			@Override
			public String getServletPath() {
				return super.getServletPath()
						+ "/"
						+ contextPathEncoder.encodeIntoPath(rpcRequest
								.getParameters());
			}

			// Force the Ajax header
			@Override
			public String getHeader(String name) {
				HttpServletRequest httpRequest = (HttpServletRequest) getRequest();
				if (httpRequest.getHeader("X-Requested-With") != null) {
					return super.getHeader(name);
				}
				if ("X-Requested-With".equals(name)) {
					return "XMLHttpRequest";
				}

				return super.getHeader(name);
			}

			// Taken from:
			// http://vangjee.wordpress.com/2009/02/25/how-to-modify-request-headers-in-a-j2ee-web-application/
			@Override
			public Enumeration getHeaderNames() {
				List list = new ArrayList();

				// loop over request headers from wrapped request object
				HttpServletRequest request = (HttpServletRequest) getRequest();
				Enumeration e = request.getHeaderNames();
				while (e.hasMoreElements()) {
					// add the names of the request headers into the list
					String n = (String) e.nextElement();
					list.add(n);
				}

				if (list.contains("X-Requested-With")) {
					return e;
				}

				// additionally, add the "username" to the list of request
				// header names
				list.add("X-Requested-With");

				// create an enumeration from the list and return
				Enumeration en = Collections.enumeration(list);
				return en;
			}
		};

		// Add the Reflection Method class to request attributes
		newRequest.setAttribute(Method.class.getName(), rpcRequest.getMethod());

		return newRequest;
	}

	protected String readContent(HttpServletRequest request)
			throws ServletException, IOException {
		return RPCServletUtils.readContentAsUtf8(request, true);
	}

	public RPCRequest decodePayload(HttpServletRequest request, String payload)
			throws SerializationException {
		return RPC.decodeRequest(payload, null, this);
	}

	// TODO I must rehearse the Details About serialization

	protected boolean shouldCompressResponse(HttpServletRequest request,
			HttpServletResponse response, String responsePayload) {
		return RPCServletUtils
				.exceedsUncompressedContentLengthLimit(responsePayload);
	}

	public final SerializationPolicy getSerializationPolicy(
			String moduleBaseURL, String strongName) {
		SerializationPolicy serializationPolicy = null;

		serializationPolicy = loadSerializationPolicy(req, moduleBaseURL,
				strongName);

		if (serializationPolicy == null) {
			// Failed to get the requested serialization policy; use the default
			logger
					.info("WARNING: Failed to get the SerializationPolicy '"
							+ strongName
							+ "' for module '"
							+ moduleBaseURL
							+ "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result.");
			serializationPolicy = RPC.getDefaultSerializationPolicy();
		}
		return serializationPolicy;
	}

	SerializationPolicy loadSerializationPolicy(HttpServletRequest request,
			String moduleBaseURL, String strongName) {
		// The request can tell you the path of the web app relative to the
		// container root.
		String contextPath = request.getContextPath();
		// String path = requestGlobals.getRequest().getContextPath();

		String modulePath = null;
		if (moduleBaseURL != null) {
			try {
				modulePath = new URL(moduleBaseURL).getPath();
			} catch (MalformedURLException ex) {
				// log the information, we will default
				logger
						.info("Malformed moduleBaseURL: {} {}", moduleBaseURL,
								ex);
			}
		}

		SerializationPolicy serializationPolicy = null;

		/*
		 * Check that the module path must be in the same web app as the servlet
		 * itself. If you need to implement a scheme different than this,
		 * override this method.
		 */
		if (modulePath == null || !modulePath.startsWith(contextPath)) {
			String message = "ERROR: The module path requested, "
					+ modulePath
					+ ", is not in the same web application as this servlet, "
					+ contextPath
					+ ".  Your module may not be properly configured or your client and server code maybe out of date.";
			logger.info(message);
		} else {
			// Strip off the context path from the module base URL. It should be
			// a
			// strict prefix.
			String contextRelativePath = modulePath.substring(contextPath
					.length());

			String serializationPolicyFilePath = SerializationPolicyLoader
					.getSerializationPolicyFileName(contextRelativePath
							+ strongName);

			Resource resource = null;
			InputStream is = null;
			try {
				resource = assetResourceLocator
						.findResourceForPath(serializationPolicyFilePath);
				is = resource.openStream();
			} catch (IOException e1) {
				logger.error("Resource lookup failed {} {}",
						serializationPolicyFilePath, e1);
			}

			try {
				if (is != null) {
					try {
						serializationPolicy = SerializationPolicyLoader
								.loadFromStream(is, null);
					} catch (ParseException e) {
						logger
								.info(
										"ERROR: Failed to parse the policy file '{}' {}",
										serializationPolicyFilePath, e);
					} catch (IOException e) {
						logger
								.info(
										"ERROR: Could not read the policy file '{}' {}",
										serializationPolicyFilePath, e);
					}
				} else {
					logger
							.info(
									"ERROR: The serialization policy file '{}' was not found; did you forget to include it in this deployment?",
									serializationPolicyFilePath);
				}
			} finally {
				if (is != null) {
					try {
						is.close();
					} catch (IOException e) {
						// Ignore this error
					}
				}
			}
		}

		return serializationPolicy;
	}

	/**
	 * A buffered output stream that
	 */
	public class GwtOutputStream extends ServletOutputStream {

		private final HttpServletResponse response;
		private final HttpServletRequest request;

		private ByteArrayOutputStream byteArrayOutputStream;

		/**
		 * Initially the ByteArrayOutputStream, later the response output stream
		 * (possibly wrapped with a GZIPOutputStream).
		 */
		private OutputStream currentOutputStream;

		public GwtOutputStream(HttpServletRequest request,
				HttpServletResponse response) {
			this.request = request;
			this.response = response;

			byteArrayOutputStream = new ByteArrayOutputStream();
			currentOutputStream = byteArrayOutputStream;
		}

		private void openResponseOutputStream() throws IOException {

			OutputStream responseOutputStream = response.getOutputStream();

			currentOutputStream = new BufferedOutputStream(responseOutputStream);

			try {
				String payload = byteArrayOutputStream.toString("UTF-8");
				System.out
						.println("GWTServletHttpRequestFilter.GwtOutputStream.openResponseOutputStream() "
								+ payload);

				if (payload.startsWith("//")) {
					// It's already encoded
					currentOutputStream.write(payload.getBytes());
				} else if (payload.startsWith("/")) {
					String encoded = RPC.encodeResponseForSuccess(
							(Method) request.getAttribute(Method.class
									.getName()), payload);
					currentOutputStream.write(encoded.getBytes());
				} else {
					// Convert to Tapestry JSONObject
					JSONObject jsonObject = new JSONObject(payload);
					if (jsonObject.has("content")) {
						payload = jsonObject.getString("content");
					} else if (jsonObject.has("redirectURL")) {
						payload = jsonObject.getString("redirectURL");
					}

					String encoded = RPC.encodeResponseForSuccess(
							(Method) request.getAttribute(Method.class
									.getName()), payload);
					// Write what content we already have to the new stream.

					currentOutputStream.write(encoded.getBytes());
				}

			} catch (SerializationException e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}

		}

		public void write(int b) throws IOException {
			currentOutputStream.write(b);
		}

		@Override
		public void write(byte[] b) throws IOException {
			currentOutputStream.write(b);
		}

		@Override
		public void write(byte[] b, int off, int len) throws IOException {
			currentOutputStream.write(b, off, len);
		}

		@Override
		public void flush() throws IOException {
			forceOutputStream().flush();
		}

		@Override
		public void close() throws IOException {
			// When closing, if we haven't accumulated enough output yet to
			// send what we have

			forceOutputStream().close();
		}

		private OutputStream forceOutputStream() throws IOException {
			if (byteArrayOutputStream != null) {
				openResponseOutputStream();
			}

			return currentOutputStream;
		}
	}

}
